package cn.kepu.elearning.util;

import cn.kepu.elearning.common.Constants;
import cn.kepu.elearning.dto.UpdateByQueryDTO;
import cn.kepu.elearning.dto.match.MatchQueryDTO;
import cn.kepu.elearning.dto.match.OtherQueryDTO;
import cn.kepu.elearning.service.SyncService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse;
import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.reindex.BulkByScrollResponse;
import org.elasticsearch.index.reindex.DeleteByQueryAction;
import org.elasticsearch.index.reindex.UpdateByQueryAction;
import org.elasticsearch.index.reindex.UpdateByQueryRequestBuilder;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;

/**
 * @author Endy
 * @create 2019-11-30 下午8:31
 **/
@Slf4j
public class ESUtil {

    private static final ESUtil ES_UTIL = new ESUtil();
    public static final String HIGHLIGHT_LABEL_PRE = "<highlight style=\"color:#f90;\">";
    public static final String HIGHLIGHT_LABEL_SUF = "</highlight>";
    private static Object LOCK = new Object();
    private static volatile Client client = null;

    /**
     *
     */
    private static final String ID = "ID";
    /**
     * 默认每页查询数据量大小
     */
    private static final int DEFAULT_SIZE = 20;
    /**
     * suggest查询的字段名称
     */
    public static final String SUGGEST_FIELD = "body";
    /**
     * 自定义的suggest名称
     */
    public static final String SUGGEST_NAME = SUGGEST_FIELD + ".suggest";


    private ESUtil() {
    }

    private static Client getClient() {
        if (client == null) {
            try {
                ES_UTIL.initClient();
            } catch (IOException e) {
                log.error("", e);
            }
        }
        return client;
    }

    /**
     * 初始化elasticsearch链接池
     *
     * @throws UnknownHostException
     */
    private void initClient() throws IOException {
        synchronized (LOCK) {
            if (client == null) {
                Properties properties = PropertiesLoaderUtils.loadProperties(
                        new EncodedResource(
                                new ClassPathResource("application.properties"), "utf-8"));
                String host = properties.getProperty("es.host");
                String port = properties.getProperty("es.port");
                if (!StringUtils.isEmpty(host)) {
                    PreBuiltTransportClient preBuiltTransportClient = new PreBuiltTransportClient(settings());
                    String[] hosts = host.split(",");
                    for (String h : hosts) {
                        try {
                            client = preBuiltTransportClient.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(h), Integer.parseInt(port)));
                            log.info("ES服务{}:{}连接成功", h, port);
                        } catch (UnknownHostException e) {
                            log.warn("ES服务{}:{}无法链接;", h, port);
                        }
                    }
                } else {
                    throw new UnknownHostException("配置文件未指定es主机;");
                }
            }
        }
    }

    private Settings settings() {
        return Settings.builder().put("cluster.name", "elasticsearch").build();
    }

    /**
     * 删除指定索引
     *
     * @param index 要删除的index
     */
    public static void deleteIndex(String index) {
        Client client = getClient();
        if (checkExists(index)) {
            client.admin().indices()
                    .prepareDelete(index)
                    .execute()
                    .actionGet();
        }
    }

    /**
     * 判断索引是否存在
     *
     * @param index 需要判断的索引
     * @return 是否存在
     */
    public static boolean checkExists(String index) {
        Client client = getClient();
        IndicesExistsResponse response = client.admin().indices()
                .prepareExists(index)
                .execute()
                .actionGet();
        return response.isExists();
    }

    /**
     * 初始化mapping信息
     *
     * @param index 要初始化的index
     */
    public static void initMappings(String index) {
        Client client = getClient();

        try {
            XContentBuilder newsBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                    .startObject("properties")
                    .startObject("createTime").field("type", "date").endObject()
                    .startObject("newsContent").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("newsId").field("type", "long").endObject()
                    .startObject("newsTitle").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("newsType").field("type", "long").endObject()
                    .startObject("orgName").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("recommendTag").field("type", "long").endObject()
                    .startObject("status").field("type", "long").endObject()
                    .startObject("tenantId").field("type", "long").endObject()
                    .endObject()
                    .endObject();

            XContentBuilder courseBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                    .startObject("properties")
                    .startObject("Classfication").field("type", "long").endObject()
                    .startObject("ClassficationName").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("Content").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("CourseType_ID").field("type", "long").endObject()
                    .startObject("Course_id").field("type", "long").endObject()
                    .startObject("Course_name").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("Course_no").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("FullPath").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("PictureURL").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("courseTypeName").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("isFirstLevelDirectory").field("type", "long").endObject()
                    .startObject("keyWords").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("openOrgSEQ").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("orgId").field("type", "long").endObject()
                    .startObject("orgName").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("pubStatus").field("type", "long").endObject()
                    .startObject("teacherId").field("type", "long").endObject()
                    .startObject("creator").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("tenantId").field("type", "long").endObject()
                    .startObject("Category").field("type", "long").endObject()
                    .startObject("isAppShow").field("type", "long").endObject()
                    .startObject("teacherName").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("selectedTimes").field("type", "long").endObject()
                    .endObject()
                    .endObject();

            XContentBuilder seriesBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                    .startObject("properties")
                    .startObject("detail").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("id").field("type", "long").endObject()
                    .startObject("isPublish").field("type", "boolean").endObject()
                    .startObject("mainPicUrl").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("name").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("openOrgSEQ").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("openScope").field("type", "long").endObject()
                    .startObject("orgId").field("type", "long").endObject()
                    .startObject("picUrl").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("publishTime").field("type", "date").endObject()
                    .startObject("sponsorInfo").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("tenantId").field("type", "long").endObject()
                    .startObject("title").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("url").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .endObject()
                    .endObject();

            XContentBuilder noticeBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                    .startObject("properties")
                    .startObject("STATUS").field("type", "long").endObject()
                    .startObject("content").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("hasSend").field("type", "long").endObject()
                    .startObject("id").field("type", "long").endObject()
                    .startObject("isPublic").field("type", "long").endObject()
                    .startObject("recommendTag").field("type", "long").endObject()
                    .startObject("tenantId").field("type", "long").endObject()
                    .startObject("tenantName").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("title").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("validDate").field("type", "date").endObject()
                    .endObject()
                    .endObject();

            XContentBuilder trainBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                    .startObject("properties")
                    .startObject("approveStatus").field("type", "long").endObject()
                    .startObject("attendants").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("comment").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("id").field("type", "long").endObject()
                    .startObject("item_type").field("type", "long").endObject()
                    .startObject("location").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("openOrgSEQ").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("orgId").field("type", "long").endObject()
                    .startObject("orgName").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("pubstatus").field("type", "long").endObject()
                    .startObject("startTime").field("type", "date").endObject()
                    .startObject("endTime").field("type", "date").endObject()
                    .startObject("tenantId").field("type", "long").endObject()
                    .startObject("trainGoal").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("trainName").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("trainTypeID").field("type", "long").endObject()
                    .startObject("trainTypeName").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("trainingContent").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("attendantCount").field("type", "long").endObject()
                    .startObject("classHour").field("type", "long").endObject()
                    .startObject("isEnrolled").field("type", "long").endObject()
                    .startObject("programStartTime").field("type", "date").endObject()
                    .startObject("programEndTime").field("type", "date").endObject()
                    .startObject("cad_report").field("type", "long").endObject()
                    .endObject()
                    .endObject();

            XContentBuilder teacherBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                    .startObject("properties")
                    .startObject("expertAreaId").field("type", "long").endObject()
                    .startObject("expertAreaName").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("headPic").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("id").field("type", "long").endObject()
                    .startObject("keyWords").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("openScope").field("type", "long").endObject()
                    .startObject("orgId").field("type", "long").endObject()
                    .startObject("remark").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("openOrgSEQ").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("teacherName").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("tenantId").field("type", "long").endObject()
                    .startObject("workPlace").field("type", "text").field("analyzer", "ik_max_word").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("post").field("type", "text").startObject("fields").startObject("keyword").field("type", "keyword").field("ignore_above", 256).endObject().endObject().endObject()
                    .startObject("hireStyle").field("type", "long").endObject()
                    .endObject()
                    .endObject();

            client.admin().indices().prepareCreate(index)
                    .addMapping(Constants.Type.NEWS, newsBuilder)
                    .addMapping(Constants.Type.COURSE, courseBuilder)
                    .addMapping(Constants.Type.SERIES, seriesBuilder)
                    .addMapping(Constants.Type.NOTICE, noticeBuilder)
                    .addMapping(Constants.Type.TRAIN, trainBuilder)
                    .addMapping(Constants.Type.TEACHER, teacherBuilder)
                    .get();
        } catch (IOException e) {
            log.error("初始化mapping失败");
        }
    }

    /**
     * 单独添加或更新mysql数据到es
     *
     * @param list    要操作的数据集合
     * @param index   es索引
     * @param type    es类型
     * @param idField 指定使用那个字段作为id
     */
    public static void addOrUpdateDocuments(List<Map<String, Object>> list, String index, String type, String idField) {
        Client client;
        try {
            client = getClient();

            BulkRequestBuilder bulkRequest = client.prepareBulk();

            for (Map<String, Object> map : list) {
                //遍历map所有field,构造插入对象
                XContentBuilder xb = XContentFactory.jsonBuilder().startObject();
                for (Map.Entry<String, Object> entry : map.entrySet()) {
                    Object tempObj = processDataFormat(entry.getValue());
                    if (org.springframework.util.StringUtils.isEmpty(tempObj) || "null".equals(tempObj)) {
                        xb.nullField(entry.getKey());
                    } else {
                        xb.field(entry.getKey(), tempObj);
                    }
                }
                xb.endObject();
                String idValue = StringUtils.trim(String.valueOf(map.get(idField)));
                UpdateRequest request = new UpdateRequest();
                request.index(index);
                request.type(type);
                request.id(idValue);
                request.upsert(xb);
                request.doc(xb);
                bulkRequest.add(request);
            }
            BulkResponse bulkResponse = bulkRequest.get();
            if (bulkResponse.hasFailures()) {
                log.warn("Elasticsearch addAndUpdate Document failures! msg:{}", bulkResponse.buildFailureMessage());
            }
        } catch (IOException e) {
            log.error("", e);
        }
    }

    private static Object processDataFormat(Object tempObj) {
        String indexOfKey = "\"";
        if (tempObj != null && tempObj.toString().contains(indexOfKey)) {
            tempObj = tempObj.toString().replaceAll("\"", "");
        }
        return tempObj;
    }

    /**
     * 指定id插入数据到es
     *
     * @param map   要操作的数据
     * @param index es索引
     * @param type  es类型
     * @param id    es中的id
     */
    public static void addDocuments(Map<String, Object> map, String index, String type, String id) {
        Client client;
        try {
            client = getClient();
            BulkRequestBuilder bulkRequest = client.prepareBulk();
            XContentBuilder xb = XContentFactory.jsonBuilder().startObject();
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                xb.field(entry.getKey(), entry.getValue());
            }
            xb.endObject();
            bulkRequest.add(client.prepareIndex(index, type, id).setSource(xb));
            BulkResponse bulkResponse = bulkRequest.get();
            if (bulkResponse.hasFailures()) {
                log.warn("Elasticsearch add Document failures! msg:{}", bulkResponse.buildFailureMessage());
            }
        } catch (IOException e) {
            log.error("", e);
        }
    }

    /**
     * 删除指定id的数据
     *
     * @param index es索引
     * @param type  es类型
     * @param id    es id
     */
    public static void delDocument(String index, String type, String id) {
        Client client = getClient();
        client.prepareDelete(index, type, id).get();
    }

    /**
     * 批量删除指定id的数据
     *
     * @param list  要删除的id集合
     * @param index es索引
     * @param type  es类型
     */
    public static void deleteDocuments(List<String> list, String index, String type) {
        Client client;
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        try {
            client = getClient();
            BoolQueryBuilder totalQuery = boolQuery();
            if (!StringUtils.isEmpty(type)) {
                totalQuery.must(termQuery("_type", type));
            }
            BoolQueryBuilder subQuery = boolQuery();
            for (String id : list) {
                subQuery.should(termQuery(ID, id));
                subQuery.minimumShouldMatch(1);
            }
            totalQuery.must(subQuery);
            BulkByScrollResponse response = DeleteByQueryAction.INSTANCE.newRequestBuilder(client)
                    .filter(totalQuery)
                    .source(index)
                    .setMaxRetries(2)
                    .abortOnVersionConflict(false)
                    .get();

            long deleted = response.getDeleted();
            log.info("成功删除数据，{}条", deleted);
        } catch (Exception e) {
            log.error("", e);
        }
    }


    public static List<Map<String, Object>> queryByType(String index, String type) {
        Client client = getClient();
        SearchRequestBuilder searchRequestBuilder = client
                .prepareSearch(index)
                .setTypes(type)
                .setFrom(0)
                .setSize(100);

        SearchResponse response = searchRequestBuilder.get();
        List<Map<String, Object>> result = new ArrayList<>();
        //遍历结果
        for (SearchHit hit : response.getHits()) {
            Map<String, Object> source = hit.getSourceAsMap();
            result.add(source);
        }
        return result;
    }

    /**
     * 根据查询条件查询
     */
    public static Map<String, Object> matchQuery(String index, MatchQueryDTO dto) {
        int pageNum = dto.getPageNum();
        int pageSize = dto.getPageSize();
        List<OtherQueryDTO> other = dto.getOther();
        String[] fieldNames = dto.getFieldNames();
        String type = dto.getType();

        Client client = getClient();
        if (pageSize <= 0) {
            pageSize = DEFAULT_SIZE;
        }
        if (pageNum < 1) {
            pageNum = 1;
        }
        int from = (pageNum - 1) * pageSize;

        BoolQueryBuilder query = boolQuery();
        if (!CollectionUtils.isEmpty(other)) {
            // 如果包含其他参数，则拼到查询语句中
            generateOtherQuery(query, other);
        }

        QueryBuilder matchQuery;
        HighlightBuilder highlightBuilder = null;
        if (StringUtils.isNotBlank(dto.getFilterText())) {
            matchQuery = QueryBuilders.multiMatchQuery(dto.getFilterText(), fieldNames).operator(Operator.AND);

            //设置高亮显示
            highlightBuilder = new HighlightBuilder().requireFieldMatch(false).fragmentSize(Integer.MAX_VALUE);
            for (String field : fieldNames) {
                highlightBuilder.field(field);
            }
            highlightBuilder.preTags(HIGHLIGHT_LABEL_PRE);
            highlightBuilder.postTags(HIGHLIGHT_LABEL_SUF);
        } else {
            matchQuery = QueryBuilders.boolQuery();
        }
        query.must(matchQuery);


        return doQuery(index, pageNum, pageSize, fieldNames, type, client, from, query, highlightBuilder);
    }

    private static Map<String, Object> doQuery(String index, int pageNum, int pageSize, String[] fieldNames, String type, Client client,
                                               int from, BoolQueryBuilder query, HighlightBuilder highlightBuilder) {
        SearchRequestBuilder searchRequestBuilder = client
                .prepareSearch(index)
                .setTypes(type)
                // .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) //设置查询类型：1.SearchType.DFS_QUERY_THEN_FETCH 精确查询； 2.SearchType.SCAN 扫描查询,无序
                .setQuery(query)
                .setFrom(from)
                .setSize(pageSize);

        if (highlightBuilder != null) {
            searchRequestBuilder.highlighter(highlightBuilder);
        }

        // 新闻 和 通知 添加按创建时间倒序排序
        if (Constants.Type.NEWS.equals(type)) {
            searchRequestBuilder.addSort("createTime", SortOrder.DESC);
        } else if (Constants.Type.NOTICE.equals(type)) {
            searchRequestBuilder.addSort("validDate", SortOrder.DESC);
        }

        SearchResponse response = searchRequestBuilder.get();
        log.debug("match查询语句为【{}】", searchRequestBuilder.toString());

        List<Map<String, Object>> result = new ArrayList<>();
        //遍历结果
        for (SearchHit hit : response.getHits()) {
            Map<String, Object> source = hit.getSourceAsMap();
            if (highlightBuilder != null) {
                //处理高亮片段
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                operateResult(source, highlightFields, fieldNames);
            }
            result.add(source);
        }

        if (highlightBuilder != null) {
            switch (type) {
                case Constants.Type.COURSE:
                    stripHtml("Content", result);
                    break;
                case Constants.Type.NEWS:
                    stripHtml("newsContent", result);
                    break;
                case Constants.Type.NOTICE:
                    stripHtml("content", result);
                    break;
                case Constants.Type.TEACHER:
                    stripHtml("remark", result);
                    break;
                case Constants.Type.SERIES:
                    stripHtml("name", result);
                    break;
                default:
                    break;
            }
        }

        // 格式化日期格式
        if (Constants.Type.TRAIN.equals(type)) {
            dateFormatter(result, SyncService.TRAIN_DATE_FIELDS);
        }
        if (Constants.Type.NEWS.equals(type)) {
            dateFormatter(result, SyncService.NEW_DATE_FIELDS);
        }
        if (Constants.Type.NOTICE.equals(type)) {
            dateFormatter(result, SyncService.NOTICE_DATE_FIELDS);
        }

        Map<String, Object> map = new HashMap<>();
        map.put("pageNum", pageNum);
        map.put("pageSize", pageSize);
        map.put("total", response.getHits().totalHits);
        map.put("list", result);
        return map;
    }

    /**
     * 格式化日期格式
     *
     * @param result 格式化以后的数据集合
     * @param fields 需要格式化的字段集合
     */
    private static void dateFormatter(List<Map<String, Object>> result, List<String> fields) {
        result.forEach(map -> {
            fields.forEach(field -> {
                if (map.containsKey(field) && null != map.get(field)) {
                    map.put(field, dateFormatter(map.get(field).toString()));
                }
            });
        });
    }

    private static String dateFormatter(String fromTime) {
        String format = null;
        try {
            format = LocalDateTime.parse(fromTime, Constants.ES_TIME_PATTERN).format(Constants.ISO_TIME_PATTERN);
        } catch (Exception e) {
            log.error("日期格式转换错误，要转换的日期为【{}】", fromTime, e);
        }
        return format;
    }

    /**
     * 去除指定字段中除了高亮标签以外的所有标签
     *
     * @param field  指定字段
     * @param result 去除标签后的数据
     */
    public static void stripHtml(String field, List<Map<String, Object>> result) {
        result.forEach(map -> {
            if (map.containsKey(field) && null != map.get(field)) {
                trimHtmlAndSubStr(field, map);
            }
        });
    }

    /**
     * 去除字符串中的标签，并且截取指定长度
     *
     * @param field 要处理的字符串
     * @param map   返回结果
     */
    private static void trimHtmlAndSubStr(String field, Map<String, Object> map) {
        Object o = map.get(field);
        int strLength = 80;
        if (o instanceof List) {
            List<String> fieldList = (List<String>) o;
            int totalLength = fieldList.stream().mapToInt(String::length).sum();
            if (totalLength > strLength) {
                int index = 0;
                // 循环来获取到要截取的index
                for (int i = 0; i < fieldList.size(); i++) {
                    int tempLength = fieldList.subList(0, i).stream().mapToInt(String::length).sum();
                    if (tempLength > strLength) {
                        index = i - 1;
                        break;
                    }
                }
                fieldList = fieldList.subList(0, index);
            }
            map.put(field, fieldList);
            return;
        }
        String val = o.toString();
        // 如果字符串本身有html标签的时候，需要用到以下注释的内容
        // val = val.replaceAll(HIGHLIGHT_LABEL_PRE, "{HIGHLIGHT_LABEL_PRE}");
        // val = val.replaceAll(HIGHLIGHT_LABEL_SUF, "{HIGHLIGHT_LABEL_SUF}");
        // val = val.replaceAll("\\<.*?>", "");
        // val = val.replaceAll("\\{HIGHLIGHT_LABEL_PRE}", HIGHLIGHT_LABEL_PRE);
        // val = val.replaceAll("\\{HIGHLIGHT_LABEL_SUF}", HIGHLIGHT_LABEL_SUF);

        int startIndex = val.indexOf(HIGHLIGHT_LABEL_SUF) - HIGHLIGHT_LABEL_PRE.length() - strLength;
        String ellipsis = "...";
        // 一个汉字相当于两个字符，所以这里的length应该写 目标长度×2的值
        int length = strLength * 2;
        if (startIndex >= 0) {
            val = val.substring(startIndex);
            val = ellipsis + val;
            length += ellipsis.length();
        }

        map.put(field, subStringHTML(val, length, ellipsis));
    }

    /**
     * @param param  将要截取的字符串参数
     * @param length 截取的字节长度
     * @param end    字符串末尾补上的字符串
     * @return 返回截取后的字符串
     * @author http://hi.baidu.com/vnplalvyulin
     * <p>
     * 按字节长度截取字符串(支持截取带HTML代码样式的字符串)
     */
    public static String subStringHTML(String param, int length, String end) {
        StringBuilder result = new StringBuilder();
        int n = 0;
        char temp;
        boolean isCode = false; //是不是HTML代码
        boolean isHTML = false; //是不是HTML特殊字符,如&nbsp;
        for (int i = 0; i < param.length(); i++) {
            temp = param.charAt(i);
            if (temp == '<') {
                isCode = true;
            } else if (temp == '&') {
                isHTML = true;
            } else if (temp == '>' && isCode) {
                n = n - 1;
                isCode = false;
            } else if (temp == ';' && isHTML) {
                isHTML = false;
            }

            if (!isCode && !isHTML) {
                n = n + 1;
                //UNICODE码字符占两个字节
                if ((temp + "").getBytes().length > 1) {
                    n = n + 1;
                }
            }

            result.append(temp);
            if (n >= length) {
                break;
            }
        }
        //取出截取字符串中的HTML标记
        String temp_result = result.toString().replaceAll("(>)[^<>]*(<?)", "$1$2");
        //去掉不需要结束标记的HTML标记
        temp_result = temp_result.replaceAll("</?(AREA|BASE|BASEFONT|BODY|BR|COL|COLGROUP|DD|DT|FRAME|HEAD|HR|HTML|IMG|INPUT|ISINDEX|LI|LINK|META|OPTION|P|PARAM|TBODY|TD|TFOOT|TH|THEAD|TR|area|base|basefont|body|br|col|colgroup|dd|dt|frame|head|hr|html|img|input|isindex|li|link|meta|option|p|param|tbody|td|tfoot|th|thead|tr)[^<>]*/?>",
                "");
        //去掉成对的HTML标记
        temp_result = temp_result.replaceAll("<([a-zA-Z]+)[^<>]*>(.*?)</\\1>", "$2");
        //用正则表达式取出标记
        Pattern p = Pattern.compile("<([a-zA-Z]+)[^<>]*>");
        Matcher m = p.matcher(temp_result);

        List<String> endHTML = new ArrayList<>();

        while (m.find()) {
            endHTML.add(m.group(1));
        }
        //补全不成对的HTML标记
        for (int i = endHTML.size() - 1; i >= 0; i--) {
            result.append("</");
            result.append(endHTML.get(i));
            result.append(">");
        }

        // 如果截取了字符串，则最后添加省略号
        if (result.length() < param.length()) {
            result.append(end);
        }

        return result.toString();
    }

    private static void generateOtherQuery(BoolQueryBuilder query, List<OtherQueryDTO> other) {
        for (OtherQueryDTO dto : other) {
            String type = dto.getFieldType() != null ? dto.getFieldType().toLowerCase() : "string";
            generateOtherQuery(query, dto, type);
        }
    }

    private static void generateOtherQuery(BoolQueryBuilder query, OtherQueryDTO dto, String type) {
        List<OtherQueryDTO> subOther = dto.getSubOther();
        if (CollectionUtils.isEmpty(subOther)) {
            generateSubQuery(query, dto, type);
        } else {
            for (OtherQueryDTO queryDTO : subOther) {
                BoolQueryBuilder subQuery = boolQuery();
                String subType = queryDTO.getFieldType() != null ? queryDTO.getFieldType().toLowerCase() : "string";
                generateOtherQuery(subQuery, queryDTO, subType);

                if (Constants.LogicOperator.MUST.equals(dto.getLogicOperator())) {
                    query.must(subQuery);
                } else if (Constants.LogicOperator.MUST_NOT.equals(dto.getLogicOperator())) {
                    query.mustNot(subQuery);
                } else {
                    query.should(subQuery);
                    query.minimumShouldMatch(1);
                }
            }
        }
    }

    private static void generateSubQuery(BoolQueryBuilder query, OtherQueryDTO dto, String type) {
        String field = dto.getField();
        if ("string".equals(type)) {
            field += ".keyword";
        }
        QueryBuilder queryBuilder;
        String operator = dto.getOperator();
        operator = StringUtils.isNotBlank(operator) ? operator.toLowerCase() : "";
        switch (operator) {
            case Constants
                    .Operator.PREFIX:
                queryBuilder = QueryBuilders.prefixQuery(field, dto.getValue());
                break;
            case Constants
                    .Operator.RANGE_GT:
                queryBuilder = QueryBuilders.rangeQuery(field).gt(dto.getValue());
                break;
            case Constants
                    .Operator.RANGE_GTE:
                queryBuilder = QueryBuilders.rangeQuery(field).gte(dto.getValue());
                break;
            case Constants
                    .Operator.RANGE_LT:
                queryBuilder = QueryBuilders.rangeQuery(field).lt(dto.getValue());
                break;
            case Constants
                    .Operator.RANGE_LTE:
                queryBuilder = QueryBuilders.rangeQuery(field).lte(dto.getValue());
                break;
            case Constants
                    .Operator.BETWEEN:
                queryBuilder = QueryBuilders.rangeQuery(field).gte(dto.getValue()).lt(dto.getSValue());
                break;
            case Constants
                    .Operator.TERM:
            default:
                queryBuilder = termQuery(field, dto.getValue());
        }
        if (Constants.LogicOperator.MUST.equals(dto.getLogicOperator())) {
            query.must(queryBuilder);
        } else if (Constants.LogicOperator.MUST_NOT.equals(dto.getLogicOperator())) {
            query.mustNot(queryBuilder);
        } else {
            query.should(queryBuilder);
            query.minimumShouldMatch(1);
        }
    }

    /**
     * 将高亮片段组装到结果中去
     *
     * @param source          要返回的结果
     * @param highlightFields 高亮字段
     * @param fieldNames      查询的字段
     */
    private static void operateResult(Map<String, Object> source, Map<String, HighlightField> highlightFields, String[] fieldNames) {
        for (String field : fieldNames) {
            HighlightField nameField = highlightFields.get(field);
            if (nameField != null) {
                Text[] fragments = nameField.fragments();
                StringBuilder nameTmp = new StringBuilder();
                for (Text text : fragments) {
                    nameTmp.append(text);
                }
                source.put(field, nameTmp.toString());
            }
        }
    }


    public static void updateByQuery(String index, UpdateByQueryDTO dto) {
        UpdateByQueryRequestBuilder updateByQuery = UpdateByQueryAction.INSTANCE.newRequestBuilder(getClient());
        String script = "ctx._source." + dto.getUpdateField() + "=params.newValue";
        Map<String, Object> map = new HashMap<>();
        map.put("newValue", dto.getUpdateValue());
        updateByQuery.source(index)
                .filter(termQuery(dto.getConditionField(), dto.getConditionValue()))
                // .size(1000)
                .script(new Script(ScriptType.INLINE, script, "painless", map));
        BulkByScrollResponse response = updateByQuery.get();
    }

    /**
     * 搜索建议
     *
     * @param param 客户端输入的内容
     * @return 查询结果
     */
    public static Map<String, Object> querySuggestion(String index, String types, String param) {
        //返回的map，进行数据封装
        Map<String, Object> msgMap = new HashMap<>();
        //创建需要搜索的inde和type
        SearchRequestBuilder requestBuilder = getClient().prepareSearch(index);
        if (StringUtils.isNotBlank(types)) {
            requestBuilder.setTypes(types.split(","));
        }
        //设置搜索建议
        CompletionSuggestionBuilder completionSuggestionBuilder = new CompletionSuggestionBuilder(SUGGEST_FIELD)
                .prefix(param.trim()).size(10);
        SuggestBuilder suggestBuilder = new SuggestBuilder().addSuggestion(SUGGEST_NAME, completionSuggestionBuilder);

        requestBuilder.suggest(suggestBuilder);
        //进行搜索
        SearchResponse suggestResponse = requestBuilder.execute().actionGet();

        //用来处理的接受结果
        List<String> result = new LinkedList<>();

        List<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> entries = suggestResponse.getSuggest()
                .getSuggestion(SUGGEST_NAME).getEntries();
        //处理结果
        for (Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option> op : entries) {
            List<? extends Suggest.Suggestion.Entry.Option> options = op.getOptions();
            for (Suggest.Suggestion.Entry.Option pp : options) {
                result.add(pp.getText().toString());
            }
        }
        msgMap.put("result", result);
        return msgMap;
    }

    /**
     * 查询总数
     *
     * @param index es索引
     * @return 符合条件的总数
     */
    public static long count(String index, MatchQueryDTO dto) {
        List<OtherQueryDTO> other = dto.getOther();
        String[] fieldNames = dto.getFieldNames();
        String type = dto.getType();

        Client client = getClient();

        BoolQueryBuilder query = boolQuery();
        if (!CollectionUtils.isEmpty(other)) {
            // 如果包含其他参数，则拼到查询语句中
            generateOtherQuery(query, other);
        }

        QueryBuilder matchQuery = QueryBuilders.multiMatchQuery(dto.getFilterText(), fieldNames).operator(Operator.AND);
        query.must(matchQuery);

        Map<String, Object> map = doQuery(index, 1, 1, fieldNames, type, client, 0, query, null);
        return Long.parseLong(map.get("total").toString());
    }

    public static void main(String[] args) {
        // matchQuery("news_website", "news", "大话西游", Arrays.asList("title", "content"), 0, 20);
        // querySuggestion("news_website", "news", "大话西游");
        String val = "测试数据，啊啊啊" + HIGHLIGHT_LABEL_PRE + "张三" + HIGHLIGHT_LABEL_SUF + "阿士" + HIGHLIGHT_LABEL_PRE;
        int startIndex = val.indexOf(HIGHLIGHT_LABEL_SUF) - HIGHLIGHT_LABEL_PRE.length() - 10;
        if (startIndex >= 0) {
            System.out.println(startIndex);
            int endIndex = val.indexOf(HIGHLIGHT_LABEL_SUF) + HIGHLIGHT_LABEL_SUF.length();
            val = val.substring(startIndex, endIndex);
        }
        System.out.println(val);
    }
}
